#ifndef IMAGE_PROCESSOR_H
#define IMAGE_PROCESSOR_H

#include <list>
#include <float.h>
#include <math.h>
#include "cglx.h"
#include "webcam.h"
#include "cloud.h"
#include "sunflower.h"
#include "weather/weatherparser.h"

// folder where images are kept
#define IMAGES_LOCATION "stitched"  
#define MAX_FRAMES 96

#define CLOUD_DEPTH_SCREEN_WIDTH 100

// width of each partition cell
#define PARTITION_WIDTH 60      

// height of each partition cell
#define PARTITION_HEIGHT 30     

// how many pixels to skip per sample
#define SAMPLING_INTERVAL 10    

// allowable noise level
#define NOISE_THRESHOLD 80      

#define TIME_TO_WAIT 0.3        // in seconds

#define ARRAY_SIZE(x) (x + PARTITION_WIDTH - 1)/(PARTITION_WIDTH)

class ImageProcessor {

    private:
        WebCam* myWebcam;
        GLuint myWidth, myHeight, myDepth;
        list<Cloud*>* myCloudList;
        list<Sunflower*>* mySunflowerList;
        weather_t* myWeather;

        GLboolean* mySeeds; 
        GLfloat myElapsedTime;

    public:
        ImageProcessor(list<Cloud*>* clouds, list<Sunflower*> *sunflowers, weather_t *weather) {
            myElapsedTime = 0;
            myWebcam = new WebCam(IMAGES_LOCATION, MAX_FRAMES);
            myWidth = myWebcam->getBackground().getImageWidth();
            myHeight = myWebcam->getBackground().getImageHeight();
            myDepth = myWebcam->getBackground().getImageDepth();

            myCloudList = clouds;
            mySunflowerList = sunflowers;
            myWeather = weather;

            mySeeds = new GLboolean[ARRAY_SIZE(myWidth)];
            memset(mySeeds, 0, sizeof(GLboolean)*ARRAY_SIZE(myWidth));
        }

        ~ImageProcessor() {
            delete myWebcam;
            delete[] mySeeds;
        }

        void update(GLfloat elapsedTime) {
            if (myElapsedTime > TIME_TO_WAIT) {
                processFrame();
                myWebcam->nextFrame();
                processClouds();
                processFlowers();
                myElapsedTime = 0;
            } else
                myElapsedTime += elapsedTime;
        }

    private:
        /*
           first sort available clouds in order of x-coordinate.
           loop through seeds, for each seed turned on, assign its position as
           target position for the first element in the list, then push the
           first element to the end of the list.
           if there are still seeds turned on but all available clouds have
           already been assigned a target, create new cloud to appear at the
           location of the seed position.
           if there are more clouds available than seeds turned on, for each of
           the unassigned (idle) clouds, check the following:
           if it is expired, remove it entirely from the list.
           if it has timed out, start it on the shrinking process.
           otherwise, decrement its countdown timer.
           */
        void processClouds() {
            myCloudList->sort(Cloud::comparePosX);
            GLuint seedsWidth = ARRAY_SIZE(myWidth);
            GLuint cloudsProcessed = 0;
            for (GLuint col = 0; col < seedsWidth; col++) {
                if (mySeeds[col]) {
                    GLfloat targetX = (col + 0.5) * CLOUD_DEPTH_SCREEN_WIDTH /
                        seedsWidth - CLOUD_DEPTH_SCREEN_WIDTH/2;
                    // if cloud available
                    if (cloudsProcessed < myCloudList->size()) {
                        Cloud* temp = myCloudList->front();
                        temp->setTargetX(targetX);
                        temp->renewLife();
                        myCloudList->push_back(temp);
                        myCloudList->pop_front();
                    }
                    else { // need to create new cloud
                        Cloud* temp = new Cloud(targetX, *myWeather == RAINY,
                                *myWeather != SUNNY);
                        myCloudList->push_back(temp);
                    }
                    cloudsProcessed++;
                }
            }

            while (cloudsProcessed < myCloudList->size()) {
                Cloud* currentCloud = myCloudList->front();
                if (!currentCloud->isExpired()) { 
                    if (!currentCloud->isAlive()) {
                        currentCloud->startShrink();
                    }
                    else {
                        currentCloud->takeLife();
                    }
                    myCloudList->push_back(currentCloud);
                    cloudsProcessed++;
                }
                myCloudList->pop_front(); // calls destructor for cloud
            }
        }

        /**
         * Turns all flowers to face a person in the scene
         * NOTE: This compiles, haven't tested
         */
/*        void processFlowers() {
            // find seeds that are "on"
            vector<GLfloat> onSeedsXValue;
            for (GLuint c = 0; c < ARRAY_SIZE(myWidth); c++) {
                if (mySeeds[c]) {
                    // Change this to reflect depth of where people walk (behind the camera)
                    GLfloat targetX = (0.5 + c) * CLOUD_DEPTH_SCREEN_WIDTH/ARRAY_SIZE(myWidth) 
                                      - CLOUD_DEPTH_SCREEN_WIDTH;
                    onSeedsXValue.push_back(targetX);
                }
            }
            
            list<Sunflower*>::iterator sfIter;
            vector<GLfloat>::iterator xIter;
            for (sfIter = mySunflowerList->begin(); sfIter != mySunflowerList->end(); sfIter++) {
                // find nearest seed, turn sunflower to face it
                GLfloat distanceToNearest = FLT_MAX;
                GLfloat toFace = 0;
                for (xIter = onSeedsXValue.begin(); xIter != onSeedsXValue.end(); xIter++) {
                    GLfloat distanceToSeed = fabs((*sfIter)->getXPosition() - *xIter);
                    if (distanceToSeed < distanceToNearest) {
                        toFace = *xIter;
                        distanceToNearest = distanceToSeed;
                    }
                }
                if (distanceToNearest != FLT_MAX) {
                    // turn sunflower to face that point
                    // (*sfIter)->facePersonAtX(toFace);
                }
            }
        }
*/

        /*
           for every sunflower, if seed of partition that sunflower falls in is
           on, jiggle sunflower
           */
        void processFlowers() {
            list<Sunflower*>::iterator iter;
            for (iter = mySunflowerList->begin(); iter !=
                    mySunflowerList->end(); iter++) {
                GLuint seedsWidth = ARRAY_SIZE(myWidth);
                GLuint col =
                    (GLuint)((*iter)->getXPosition()+CLOUD_DEPTH_SCREEN_WIDTH/2)*seedsWidth/CLOUD_DEPTH_SCREEN_WIDTH;
                if (mySeeds[col]) {
					(*iter)->setJiggling();
                }
            }
        }
        /**
          * processes frame by detecting presence of objects from finding the
          * difference with a background image. 
          * 
          * image is divided into partitions, each with attributes specified by
          * PARTITION_WIDTH and PARTITION_HEIGHT. for each partition, or cell,
          * take samples at intervals specified by SAMPLE_INTERVAL. In essence,
          * further divide up each partition further into sub-partitions and
          * test whether the center of each sub-partition is noticeably
          * different from background image.
          *
          * within each partition cell, if there are more than 1 sub-partition
          * that gets filtered through, we assume there is sufficient action
          * going on to be detected as an object, thus flipping the
          * corresponding boolean value.
          **/
        void processFrame() {
            Texture current = myWebcam->getCurrentFrame();
            Texture background = myWebcam->getBackground();
            GLubyte* rawData = current.getImageData();
            GLubyte* bgData = background.getImageData();
            memset(mySeeds, 0, sizeof(GLuint)*ARRAY_SIZE(myWidth));

            // iterate through cells
            for (GLuint h = 0; h < myHeight; h += PARTITION_HEIGHT) {
                for (GLuint w = 0; w < myWidth; w += PARTITION_WIDTH) {
                    GLuint count = 0;
                    // sample each cell
                    for (GLuint row = SAMPLING_INTERVAL/2; row < PARTITION_HEIGHT; row += SAMPLING_INTERVAL) {
                        for (GLuint col = SAMPLING_INTERVAL; col < PARTITION_WIDTH; col += SAMPLING_INTERVAL) {
                            GLuint k = ((h + row)*myWidth + w + col)*myDepth;
                            GLint rdiff = GLint(rawData[k])   - GLint(bgData[k]);
                            GLint gdiff = GLint(rawData[k+1]) - GLint(bgData[k+1]);
                            GLint bdiff = GLint(rawData[k+2]) - GLint(bgData[k+2]);
                            if (abs(rdiff) > NOISE_THRESHOLD || abs(gdiff) > NOISE_THRESHOLD || abs(bdiff) > NOISE_THRESHOLD) {
                                count++;
                            }
                        }
                    }
                    // do manipulations here
                    if (count > 1) {
                        mySeeds[w/PARTITION_WIDTH] = 1;
                    }
                    // std::cout << count << "\t";
                }
                // std::cout << "\n";
            }
            // std::cout << "\n";
        }
};

#endif
